package com.xiyang.web.configuration.security.authorization;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.*;
import java.io.IOException;

/**
 *
 * AbstractSecurityInterceptor
 * 为安全对象时间安全拦截的抽象类
 * AbstractSecurityInterceptor 将确保安全拦截器的正确启动配置。他还将实现对安全对象调用的正确处理，即：
 *
 *      从SecurityContentHolder获取Authentication对象
 *      根据权限资源（SecurityMetadataSource）查找安全对象请求，确保请求是否与安全或公共调用相关。
 *
 *      对于安全的调用（安全对象调用有一个ConfigAttribute列表）：
 *          1.如果isAuthenticated()返回false，或者alwaysReauthenticate是true,根据配置的AuthenticationManager对请求进行身份验证。
 *          经过身份验证后，将SecurityContextHolder上的身份验证对象替换为返回值。
 *          2.针对配置的AccessDecisionManager授权请求
 *          3.通过配置的RunAsManager执行替换一个run-as。
 *          4.将控件传递回具体的子类，该子类将实际继续执行对象。
 *          5.返回一个InterceptorStatusToken,以便在子类完成对象的执行之后，其finally子句可以确保重新调用
 *          AbstractSecurityInterceptor，并使用finallyInvocation(InterceptorStatusToken)正确处理。
 *          6.具体的子类将通过afterInvocation(InterceptorStatusToken,Object)方法重新调用AbstractSecurityInterceptor。
 *          7.如果RunAsManager替换了验证身份验证对象，请将SecurityContextHolder返回到调用AuthenticationManager。
 *          8.如果定义了AuthenticationManager,请调用 调用管理器（invocation manager） 并允许它替换由于返回给调用方而导致的对象。
 *
 *
 *      对于公共的调用（没有ConfigAttribute用于安全对象调用）：
 *          如上所述，具体的子类将返回一个InterceptorStatusToken，在安全对象被执行之后，InterceptorStatusToken将被重新呈现给AbstractSecurityInterceptor。
 *          当调用AbstractSecurityInterceptor的afterInvocation(InterceptorStatusToken,Object)时，它将不采取进一步的操作。
 *          控件再次返回到具体的子类，以及应返回给调用方法的对象。然后，子类会将该结果或异常返回给原始调用方。
 *
 *
 *
 * 访问资源（即授权管理），访问url时，会通过AbstractSecurityInterceptor拦截器拦截，
 * 其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限，
 * 在调用授权管理器AccessDecisionManager，这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息，
 * 还会获取被拦截的url和被拦截url所需的全部权限，然后根据所配的策略（有：一票决定，一票否定，少数服从多数等），
 * 如果权限足够，则返回，权限不够则报错并调用权限不足页面
 *
 *
 *
 * 注意:在spring容器托管的AbstractSecurityInterceptor的bean，都会自动加入到servlet的filter chain，
 * 解决方法：1.new CustomFilterSecurityInterceptor（）
 * 2. 做个标记
 * @author xiyang.ycj
 * @since Jun 26, 2019 11:09:31 AM
 */
@Component
public class CustomFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

    private static final Logger log = LoggerFactory.getLogger(CustomFilterSecurityInterceptor.class);

    private static final String FILTER_APPLIED = "__spring_security_CustomFilterSecurityInterceptor_filterApplied";


    private final CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService;
    private final CustomAccessDecisionManager customAccessDecisionManager;

    @Autowired
    public CustomFilterSecurityInterceptor(CustomInvocationSecurityMetadataSourceService customInvocationSecurityMetadataSourceService, CustomAccessDecisionManager customAccessDecisionManager) {
        this.customInvocationSecurityMetadataSourceService = customInvocationSecurityMetadataSourceService;
        this.customAccessDecisionManager = customAccessDecisionManager;
    }

    /**
     *  初始化时将定义的DecisionManager,注入到父类AbstractSecurityInterceptor中
     *  注意：
     *  @PostConstruct 用于在依赖关系注入完成之后需要执行的方法，以执行任何初始化。
     *  此方法必须在将类放入服务之前调用，且只执行一次。
     */
    @PostConstruct
    public void init(){
        log.info("设置==========================================鉴权决策管理器");
        super.setAccessDecisionManager(customAccessDecisionManager);
    }

    /**
     * 向父类提供要处理的安全对象类型，因为父亲被调用的方法参数类型大多是Object，框架需要保证传递进去的安全对象类型相同
     *
     * @return 子类为其提供服务的安全对象的类型
     */
    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    /**
     * 获取到自定义MetadataSource的方法
     *
     * 启动时会有3次调用
     * 第一次调用：{@link AbstractSecurityInterceptor#afterPropertiesSet()} 135行
     * 第二次调用：{@link AbstractSecurityInterceptor#afterPropertiesSet()} 137行
     * 第三次调用：{@link AbstractSecurityInterceptor#afterPropertiesSet()} 156行
     *
     * 登录时调用
     * 调用：{@link AbstractSecurityInterceptor#beforeInvocation(Object)} 196行
     *
     * @return  权限资源映射的数据源
     */
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.customInvocationSecurityMetadataSourceService;
    }

    /**
     *
     * 由Web容器调用，以向filter指示正在将其放入服务中。
     * servlet容器在实例化filter后，只调用一个init方法。
     * 在要求filter执行任何过滤之前，init方法必须成功完成。
     * 如果init方法满足以下条件之一，则web容器无法将筛选器放入服务：抛出 servletException
     * 在web容器定义的时间内不返回
     * 默认实现时NO-OP
     * @param filterConfig 与正在初始化的filter实例关联的配置信息
     * @throws ServletException 如果实例化失败
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("filer==========================================init");
    }

    /**
     * 每当request/response对由于客户端请求链末端的资源而通过链时，容器调用过滤器的doFilter方法。
     * 传入此方法的filter chain 允许Filter传递请求并响应链中的下一个实体。
     *
     * 此方法的典型实现将遵循以下模式：
     * 1.检查请求
     * 2.也可以使用自定义实现包装请求对象，输入filter的内容或头
     * 3.（可选）使用自定义实现包装响应对象，以Filter 内容或头进行输出过滤
     * 4.使用FilterChain对象的chain.doFilter()调用链中的下一个实体
     * 5.在调用FilterChain中的下一个实体后，直接在响应上设置头。
     * @param request  要处理的请求
     * @param response 与请求关联的响应
     * @param chain   提供对链中下一个Filter的访问，以便此Filter将请求和响应传递给以进行进一步处理
     * @throws IOException      如果在此筛选器处理请求期间发生I/O错误
     * @throws ServletException 如果由于其他原因处理失败
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("[自定义过滤器]：{}","CustomFilterSecurityInterceptor.doFilter()");
        FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
        invoke(filterInvocation);
    }

    /**
     * 由Web容器调用，以向filter指示它正在退出服务。
     * 只有当filter的doFilter方法中的所有线程都退出或超出时间间后，才调用此方法。
     * 在web容器调用此方法之后，它将不再在此filter的实例上调用doFilter方法。
     * 此方法使filter有机会clean正在保留的任何资源（例如内存、文件句柄、线程）
     * 并确保任何持久状态与filter在内存中的当前状态同步。
     *
     * 默认实现时NO-OP
     */
    @Override
    public void destroy() {
        log.info("filer==========================================destroy");
    }


    private void invoke(FilterInvocation fi) throws IOException, ServletException {
        if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)) {
            // filter already applied to this request and user wants us to observe
            // once-per-request handling, so don't re-do security checking
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        }
        else {
            // first time this request being called, so perform security checking
            if (fi.getRequest() != null ) {
                fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
            }

            /* 调用父类的beforeInvocation ==> accessDecisionManager.decide(..) */
            InterceptorStatusToken token = super.beforeInvocation(fi);

            try {
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            }
            finally {
                super.finallyInvocation(token);
            }
            super.afterInvocation(token, null);
        }
    }
}
